﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Text;

namespace Gargoyle.Roton
{
    /// <summary>
    /// The base video interface for Roton.
    /// </summary>
    public class Video
    {
        /// <summary>
        /// Total number of characters in the video matrix.
        /// </summary>
        protected int Area;

        /// <summary>
        /// Character to be used for clearing the video matrix.
        /// </summary>
        protected AnsiChar BlankChar;

        /// <summary>
        /// Video matrix data.
        /// </summary>
        protected AnsiChar[] Grid;

        /// <summary>
        /// Size of the video matrix, in characters.
        /// </summary>
        protected Size Size;

        /// <summary>
        /// Create a new video interface.
        /// </summary>
        /// <param name="newSize">The size, in characters, of the video interface.</param>
        public Video(Size newSize)
        {
            SetSize(newSize);
        }

        /// <summary>
        /// Clear the video matrix.
        /// </summary>
        public void Clear()
        {
            Clear(BlankChar);
        }

        /// <summary>
        /// Clear the video matrix with a specific character.
        /// </summary>
        /// <param name="ac">The AnsiChar to use for clearing.</param>
        public void Clear(AnsiChar ac)
        {
            for (int i = 0; i < Area; i++)
            {
                SetChar(GridPoint(i), ac);
            }
        }

        /// <summary>
        /// Get the AnsiChar at a position in the video matrix.
        /// </summary>
        /// <param name="p">The location to retrieve the character from.</param>
        /// <returns>The character returned from the specified location.</returns>
        public virtual AnsiChar GetChar(Point p)
        {
            int index = GridIndex(p);
            if ((index >= 0) && (index < Area))
                return Grid[GridIndex(p)];
            return BlankChar;
        }

        /// <summary>
        /// Get a rectangular section of the video matrix.
        /// </summary>
        /// <param name="sourceRect">The rectangle to retrieve the characters from.</param>
        /// <returns>VideoSection containing the characters.</returns>
        public VideoSection GetSection(Rectangle sourceRect)
        {
            VideoSection result = new VideoSection();
            int sectionArea = sourceRect.Width * sourceRect.Height;
            int sectionIndex = 0;

            result.Width = sourceRect.Width;
            result.Height = sourceRect.Height;
            result.Chars = new AnsiChar[sectionArea];

            for (int y = 0; y < sourceRect.Height; y++)
            {
                for (int x = 0; x < sourceRect.Width; x++)
                {
                    result.Chars[sectionIndex] = GetChar(new Point(x + sourceRect.Left, y + sourceRect.Top));
                    sectionIndex++;
                }
            }

            return result;
        }

        /// <summary>
        /// Get the size of the video matrix in characters.
        /// </summary>
        /// <returns>The size of the video matrix in characters.</returns>
        public virtual Size GetSize()
        {
            return Size;
        }

        /// <summary>
        /// Get the index in the video matrix data for a particular location.
        /// </summary>
        /// <param name="p">The location to get the index for.</param>
        /// <returns>The index for the location.</returns>
        protected int GridIndex(Point p)
        {
            return p.X + (p.Y * Size.Width);
        }

        /// <summary>
        /// Get the location in the video matrix for a particular index.
        /// </summary>
        /// <param name="index">The index to get the location for.</param>
        /// <returns>The location for the index.</returns>
        protected Point GridPoint(int index)
        {
            return new Point(index % Size.Width, index / Size.Width);
        }

        /// <summary>
        /// Resize the video matrix.
        /// </summary>
        /// <param name="newSize">The new size, in characters.</param>
        public virtual void Resize(Size newSize)
        {
            SetSize(newSize);
        }

        /// <summary>
        /// Set a character at a location in the video matrix.
        /// </summary>
        /// <param name="p">The location in the video matrix.</param>
        /// <param name="ac">The character data to be stored in the video matrix.</param>
        public virtual void SetChar(Point p, AnsiChar ac)
        {
            int index = GridIndex(p);
            if ((index >= 0) && (index < Area))
                Grid[GridIndex(p)] = ac;
        }

        /// <summary>
        /// Store a rectangular section of characters into the video matrix.
        /// </summary>
        /// <param name="p">The upper-left location where to store the characters.</param>
        /// <param name="vs">The VideoSection which contains the characters to be stored.</param>
        public void SetSection(Point p, VideoSection vs)
        {
            int selectionIndex = 0;

            for (int y = 0; y < vs.Height; y++)
            {
                for (int x = 0; x < vs.Width; x++)
                {
                    SetChar(new Point(p.X + x, p.Y + y), vs.Chars[selectionIndex]);
                    selectionIndex++;
                }
            }
        }

        /// <summary>
        /// Set the size of the video matrix.
        /// </summary>
        /// <param name="newSize">The new size for the video matrix, in characters.</param>
        protected virtual void SetSize(Size newSize)
        {
            Size = newSize;
            Area = Size.Width * Size.Height;
            Grid = new AnsiChar[Area];
            BlankChar = new AnsiChar(0, 0);
        }

        /// <summary>
        /// Determine if the location is within the video matrix.
        /// </summary>
        /// <param name="p">The location to check.</param>
        /// <returns>If true, the location is within the video matrix.</returns>
        protected bool WithinGrid(Point p)
        {
            if ((p.X >= 0) && (p.X < Size.Width) && (p.Y >= 0) && (p.Y < Size.Height))
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Write a string into the video matrix.
        /// </summary>
        /// <param name="p">Location at which to draw the string.</param>
        /// <param name="bytes">The string data to be written.</param>
        /// <param name="color">The ANSI color to use for the written string.</param>
        public void WriteString(Point p, ByteString bytes, byte color)
        {
            Point point = p;
            int length = bytes.Length;

            for (int i = 0; i < length; i++)
            {
                SetChar(point, new AnsiChar(bytes[i], color));
                point.X++;
            }
        }
    }

    public class TextModeVideo : Video, IDisposable
    {
        /// <summary>
        /// Event to trigger when the bitmap has changed.
        /// </summary>
        public EventHandler BitmapInvalidated;

        private const int FontWidth = 8;
        private const int TotalColors = 16;

        private class Glyph
        {
            public const Int32 MaskEnable = -1;
            public const Int32 MaskDisable = -16777216;

            public Int32[] Mask;
            public Int32[] Inverse;

            public Glyph(byte[] newMask)
            {
                int count = newMask.Length;
                int maskIndex = 0;
                byte bitData;

                Mask = new Int32[count * FontWidth];
                Inverse = new Int32[Mask.Length];

                for (int i = 0; i < count; i++)
                {
                    bitData = newMask[i];
                    for (int j = 0; j < FontWidth; j++)
                    {
                        if ((bitData & (byte)0x80) != 0)
                        {
                            Mask[maskIndex] = MaskEnable;
                            Inverse[maskIndex] = MaskDisable;
                        }
                        else
                        {
                            Mask[maskIndex] = MaskDisable;
                            Inverse[maskIndex] = MaskEnable;
                        }
                        maskIndex++;
                        bitData <<= 1;
                    }
                }
            }
        }

        private delegate byte ColorFunction(byte color);
        private delegate void UpdateFunction(Point p);

        private Bitmap bitmap;
        private bool disposed;
        
        private bool BlinkOn;
        private bool BrightMode;
        private Int32[] Colors;
        private Size FontSize;
        private ColorFunction GetColor;
        private int GlyphLength;
        private Glyph[] Glyphs;
        private Int32[] Pixels;
        private GCHandle PixelsHandle;
        private int Stride;
        private UpdateFunction UpdateChar;
        private int WidthInPixels;
        private int XScale;

        /// <summary>
        /// Create a new video interface that emulates IBM text mode.
        /// </summary>
        /// <param name="newSize">The size of the video matrix, in characters.</param>
        /// <param name="newFont">The raw font data to use. If null, the default code page 437 character set will be used.</param>
        /// <param name="newPalette">The palette data to use. If null, the IBM text mode palette will be used.</param>
        /// <param name="wideMode">If true, characters will be drawn twice as wide.</param>
        public TextModeVideo(Size newSize, byte[] newFont, Color[] newPalette, bool wideMode) : base(newSize)
        {
            if (wideMode)
                XScale = 2;
            else
                XScale = 1;

            BuildGlyphs(newFont);
            BuildColors(newPalette);
            BuildBitmap();
            SetBlink(true);
            SetBright(false);
            Clear();
        }

        ~TextModeVideo()
        {
            if (!disposed)
                Dispose();
        }

        /// <summary>
        /// The bitmap rendered from the video matrix.
        /// </summary>
        public Bitmap Bitmap
        {
            get
            {
                return bitmap;
            }
        }

        /// <summary>
        /// The pointer to the raw bitmap bits.
        /// </summary>
        public IntPtr BitsHandle
        {
            get
            {
                return PixelsHandle.AddrOfPinnedObject();
            }
        }

        private void BuildBitmap()
        {
            WidthInPixels = FontSize.Width * Size.Width * XScale;
            int height = FontSize.Height * Size.Height;

            int pixelArea = WidthInPixels * height;

            Stride = WidthInPixels * 4;
            Pixels = new Int32[pixelArea];
            PixelsHandle = GCHandle.Alloc(Pixels, GCHandleType.Pinned);
            bitmap = new Bitmap(WidthInPixels, height, Stride, PixelFormat.Format32bppPArgb, PixelsHandle.AddrOfPinnedObject());

            for (int i = 0; i < pixelArea; i++)
            {
                Pixels[i] = Colors[0];
            }
        }

        private void BuildColors(Color[] newPalette)
        {
            if (newPalette == null)
                newPalette = GetDefaultPalette();

            Colors = new Int32[TotalColors];

            for (int i = 0; i < TotalColors; i++)
            {
                Colors[i] = (Int32)newPalette[i].ToArgb();
            }
        }

        private void BuildGlyphs(byte[] newFont)
        {
            if (newFont == null)
                newFont = GetDefaultFont();

            Glyphs = new Glyph[256];
            FontSize = new Size(FontWidth, newFont.Length / 256);
            GlyphLength = FontSize.Width * FontSize.Height;

            byte[] charData = new byte[FontSize.Height];
            int fontIndex = 0;
            int glyphIndex = 0;
            int count = newFont.Length;

            while (fontIndex < count)
            {
                Array.Copy(newFont, fontIndex, charData, 0, FontSize.Height);
                fontIndex += FontSize.Height;
                Glyphs[glyphIndex] = new Glyph(charData);
                glyphIndex++;
            }
        }

        public void Dispose()
        {
            if (!disposed)
            {
                PixelsHandle.Free();
                bitmap.Dispose();
                disposed = true;
            }
        }

        private void DrawGlyph(Point p, Glyph glyph, Int32 foreColor, Int32 backColor)
        {
            int baseOffset = (p.X * FontSize.Width * XScale) + (p.Y * FontSize.Height * WidthInPixels);
            int glyphOffset = 0;
            int pixelOffset;

            while (glyphOffset < GlyphLength)
            {
                pixelOffset = baseOffset;
                baseOffset += WidthInPixels;
                for (int i = 0; i < FontSize.Width; i++)
                {
                    for (int j = 0; j < XScale; j++)
                    {
                        Pixels[pixelOffset] = (foreColor & glyph.Mask[glyphOffset]) | (backColor & glyph.Inverse[glyphOffset]);
                        pixelOffset++;
                    }
                    glyphOffset++;
                }
            }
        }

        private byte GetColorNormal(byte color)
        {
            return (byte)(color & 0x7F);
        }

        private byte GetColorBright(byte color)
        {
            return color;
        }

        private byte[] GetDefaultFont()
        {
            return Properties.Resources.vga_rom;
        }

        private Color[] GetDefaultPalette()
        {
            Color[] defaultColors = { 
                                        Color.FromArgb(0xFF, 0x00, 0x00, 0x00),
                                        Color.FromArgb(0xFF, 0x00, 0x00, 0xAA),
                                        Color.FromArgb(0xFF, 0x00, 0xAA, 0x00),
                                        Color.FromArgb(0xFF, 0x00, 0xAA, 0xAA),
                                        Color.FromArgb(0xFF, 0xAA, 0x00, 0x00),
                                        Color.FromArgb(0xFF, 0xAA, 0x00, 0xAA),
                                        Color.FromArgb(0xFF, 0xAA, 0x55, 0x00),
                                        Color.FromArgb(0xFF, 0xAA, 0xAA, 0xAA),
                                        Color.FromArgb(0xFF, 0x55, 0x55, 0x55),
                                        Color.FromArgb(0xFF, 0x55, 0x55, 0xFF),
                                        Color.FromArgb(0xFF, 0x55, 0xFF, 0x55),
                                        Color.FromArgb(0xFF, 0x55, 0xFF, 0xFF),
                                        Color.FromArgb(0xFF, 0xFF, 0x55, 0x55),
                                        Color.FromArgb(0xFF, 0xFF, 0x55, 0xFF),
                                        Color.FromArgb(0xFF, 0xFF, 0xFF, 0x55),
                                        Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)
                                    };
            return defaultColors;
        }

        /// <summary>
        /// Retrieves the current font being used by the TextModeVideo instance.
        /// </summary>
        /// <returns>A byte array containing the font.</returns>
        public byte[] GetFont()
        {
            byte[] outFont = new byte[Glyphs.Length * FontSize.Height];

            for(int i = 0; i < Glyphs.Length; i++)
            {
                int fontOffset = i * FontSize.Height;

                for(int y = 0; y < FontSize.Height; y++)
                {
                    byte bitMask = 0;

                    for(int x = 0; x < FontSize.Width; x++)
                    {
                        int maskOffset = x + (y * FontSize.Width);
                        bitMask <<= 1;

                        if(Glyphs[i].Mask[maskOffset] == Glyph.MaskEnable)
                        {
                            bitMask ^= 1;
                        }
                    }

                    outFont[fontOffset + y] = bitMask;
                }
            }

            return outFont;
        }

        /// <summary>
        /// Retrieves the current palette being used by the TextModeVideo instance.
        /// </summary>
        /// <returns>A Color array containing the palette.</returns>
        public Color[] GetPalette()
        {
            Color[] outPalette = new Color[Colors.Length];

            for(int i = 0; i < Colors.Length; i++)
            {
                outPalette[i] = Color.FromArgb(Colors[i]);
            }

            return outPalette;
        }

        private void RedrawScreen()
        {
            for(int i = 0; i < Area; i++)
                UpdateChar(GridPoint(i));
        }

        /// <summary>
        /// Resize the video matrix.
        /// </summary>
        /// <param name="newSize">The new size for the video matrix, in characters.</param>
        public override void Resize(Size newSize)
        {
              base.Resize(newSize);
             BuildBitmap();
             Clear();
        }

        /// <summary>
        /// Set the blink mode.
        /// </summary>
        /// <param name="newBlink">If true, blinking characters will be shown. If false, they will be hidden.</param>
        public void SetBlink(bool newBlink)
        {
            if (BlinkOn != newBlink)
            {
                BlinkOn = newBlink;
                if (!BrightMode)
                {
                    SetColorFunctions();
                    UpdateBlinkChars();
                }
            }
        }

        /// <summary>
        /// Set bright mode.
        /// </summary>
        /// <param name="newBright">If true, blinking characters will be disabled and all 16 colors can be used for the background.</param>
        public void SetBright(bool newBright)
        {
            if (BrightMode != newBright)
            {
                BrightMode = newBright;
                SetColorFunctions();
                UpdateBlinkChars();
            }
        }

        /// <summary>
        /// Set the character at a location in the video matrix.
        /// </summary>
        /// <param name="p">The location in the video matrix to set the character.</param>
        /// <param name="ac">The character data to be used.</param>
        public override void SetChar(Point p, AnsiChar ac)
        {
            if (WithinGrid(p))
            {
                AnsiChar currentChar = GetChar(p);
                if ((currentChar.Character != ac.Character) || (currentChar.Color != ac.Color))
                {
                    base.SetChar(p, ac);
                    UpdateChar(p);
                }
            }
        }

        private void SetColorFunctions()
        {
            if (BrightMode)
            {
                UpdateChar = UpdateCharBlinkOn;
                GetColor = GetColorBright;
            }
            else
            {
                GetColor = GetColorNormal;
                if (BlinkOn)
                    UpdateChar = UpdateCharBlinkOn;
                else
                    UpdateChar = UpdateCharBlinkOff;
            }
        }

        /// <summary>
        /// Sets the font that a given TextModeVideo instance uses.
        /// </summary>
        /// <param name="newFont">A byte array containing the font data.</param>
        public void SetFont(byte[] newFont)
        {
            SetFont(newFont, true);
        }

        /// <summary>
        /// Sets the font that a given TextModeVideo instance uses.
        /// </summary>
        /// <param name="newFont">A byte array containing the font data.</param>
        /// <param name="rebuildBitmap">Specifies whether or not to invalidate the existing bitmap (this must be done when changing the font size!).</param>
        public void SetFont(byte[] newFont, bool rebuildBitmap)
        {
            byte[] adjustedFont;
            int doublingThreshold = Convert.ToInt32(ConfigManager.GetValue("Video", "DoubleFontHeightThreshold", "8"));

            // check to see if the font should be automatically doubled vertically
            int lines = newFont.Length / 256;
            if(lines <= doublingThreshold)
            {
                adjustedFont = new byte[newFont.Length * 2];

                for(int i = 0; i < newFont.Length; i++)
                    adjustedFont[(i * 2)] = adjustedFont[(i * 2) + 1] = newFont[i];

                newFont = adjustedFont;
            }

            BuildGlyphs(newFont);
            if(rebuildBitmap)
            {
                BuildBitmap();
                if(BitmapInvalidated != null)
                    BitmapInvalidated(this, new EventArgs());
            }
            RedrawScreen();
        }

        /// <summary>
        /// Sets the palette that a given TextModeVideo instance uses.
        /// </summary>
        /// <param name="newPalette">A Color array containing the palette data.</param>
        public void SetPalette(Color[] newPalette)
        {
            BuildColors(newPalette);
            RedrawScreen();
        }
        
        private void UpdateBlinkChars()
        {
            for (int i = 0; i < Area; i++)
            {
                if ((Grid[i].Color & 0x80) != 0)
                {
                    UpdateChar(GridPoint(i));
                }
            }
        }

        private void UpdateCharBlinkOff(Point p)
        {
            AnsiChar ac = GetChar(p);
            if ((ac.Color & 0x80) != 0)
            {
                ac.Color = GetColor(ac.Color);
                DrawGlyph(p, Glyphs[ac.Character], Colors[ac.Color >> 4], Colors[ac.Color >> 4]);
            }
            else
            {
                UpdateCharBlinkOn(p);
            }
        }

        private void UpdateCharBlinkOn(Point p)
        {
            AnsiChar ac = GetChar(p);
            ac.Color = GetColor(ac.Color);
            DrawGlyph(p, Glyphs[ac.Character], Colors[ac.Color & 0xF], Colors[ac.Color >> 4]);
        }
    }
}
